Fibers
=======

.. index::
    fiber

Fiber - это поток выполнения, позволяющий одному потоку решать несколько задач.
По сравнению с обычными потоками, которые обычно используются в параллелизме и
параллелизма и кщнкурентном исполенении, эффективнее переключаться между волокнами. Волокна похожи на
корутины и зеленые потоки.
Fiber позволяют использовать несколько стеков вызовов в одном потоке. По этой причине, чтобы полностью
понять и оценить волокна, необходимо сначала понять стек вызовов потока
потока

Стек вызовов
-------------

Параметры, нестатические локальные переменные, возвращаемое значение и временные
выражения функции, а также любая дополнительная информация, которая может потребоваться
во время ее выполнения, составляют локальное состояние функции. Локальное
состояние функции выделяется и инициализируется автоматически во время выполнения при каждом
при каждом вызове этой функции.
Место, отведенное для хранения локального состояния при вызове функции, называется фреймом
(или фреймом стека). Поскольку функции вызывают другие функции во время выполнения потока,
их фреймы концептуально помещаются друг на друга, образуя стек
фреймов. Стек фреймов активных в данный момент вызовов функций является стеком вызовов
этого потока.
Например, в момент, когда главный поток следующей программы начинает
выполнение функции bar(), будет три уровня активных вызовов функций
из-за того, что main() вызывает foo() и foo() вызывает bar():

.. code-block:: d

    void main()
    {
        int a;
        int b;
        int c = foo(a, b);
    }

    int foo(int x, int y)
    {
        bar(x + y);
        return 42;
    }

    void bar(int param)
    {
        string[] arr;
        // ...
    }

Во время выполнения bar() стек вызовов будет состоять из трех фреймов
хранящих локальные состояния активных в данный момент вызовов функций

.. image:: images/87_1.png

Поскольку слои вызовов функций становятся глубже, когда функции вызывают другие функции, и
более мелкие, когда функции возвращаются, размер стека вызовов увеличивается и
соответственно уменьшается. Например, когда функция bar() возвращается, ее кадр больше не нужен, и его место впоследствии будет использовано для вызова другой функции в
больше не нужен, и его место впоследствии будет использовано для вызова другой функции в
в будущем:

.. image:: images/87_2.png

Мы использовали преимущества стека вызовов в каждой программе, которую мы
написали до сих пор. Преимущества стека вызовов особенно очевидны для рекурсивных
функций.

.. index::
    рекурсия
    recursion

**Рекурсия**

Рекурсия - это ситуация, когда функция вызывает сама себя прямо или косвенно.
Рекурсия значительно упрощает некоторые виды алгоритмов, например те, которые
классифицируются как "разделяй и властвуй".
Рассмотрим следующую функцию, которая вычисляет сумму элементов
среза. Она решает эту задачу, рекурсивно вызывая саму себя с фрагментом, который на один
элемент короче, чем полученный. Рекурсия продолжается до тех пор, пока
пока срез не станет пустым. Текущий результат переносится на следующий шаг рекурсии
в качестве второго параметра:

.. code-block:: d

    import std.array;

    int sum(int[] arr, int currentSum = 0)
    {
        if (arr.empty)
        {
            /* No element to add. The result is what has been
    * calculated so far. */
            return currentSum;
        }
        /* Add the front element to the current sum and call self
    * with the remaining elements. */
        return sum(arr[1 .. $], currentSum + arr.front);
    }

    void main()
    {
        assert(sum([1, 2, 3]) == 6);
    }

*Примечание: приведенный выше код предназначен только для демонстрации. В противном случае сумму элементов
диапазона должна вычисляться с помощьюstd.algorithm.sum, который использует специальные алгоритмы для
для более точных вычислений для типов с плавающей точкой.*
Когда в конечном итоге вызывается sum() с пустым срезом для начального аргумента
[1, 2, 3], соответствующие части стека вызовов будут состоять из
следующие кадры. Значение каждого параметра указывается после знака ==.
Не забывайте читать содержимое фрейма снизу вверх:

.. image:: images/87_3.png



*Примечание:На практике, когда рекурсивная функция непосредственно возвращает результат вызова самой себя,
компиляторы используют технику, называемую "оптимизацией хвостового вызова", которая устраняет отдельные
кадров для каждого рекурсивного вызова.*
В многопоточной программе, поскольку каждый поток будет работать над своей
задачей, каждый поток получает свой собственный стек вызовов для поддержания своего состояния выполнения.
Сила fibers основана на том, что хотя fiber не является потоком, он
получает свой собственный стек вызовов, что позволяет использовать несколько стеков вызовов для каждого потока. Поскольку
один стек вызовов поддерживает состояние выполнения одной задачи, несколько стеков вызовов
позволяют потоку работать над несколькими задачами.

Использование
-------------

Ниже перечислены распространенные операции с fibers. Мы увидим примеры этих операций
ниже.
* Fiber начинает выполнение с вызываемой сущности (указатель функции, делегат,
и т. д.), которая не принимает никаких параметров и ничего не возвращает. Для
Например, следующая функция может быть использована в качестве функции fiber:

.. code-block:: d

    void fiberFunction()
    {
    // ...
    }

* Fiber может быть создано как объект класса core.thread.Fiber с вызываемой сущностью:

.. code-block:: d

    import core.thread;
    // ...
    auto fiber = new Fiber(&fiberFunction)

В качестве альтернативы можно определить подкласс Fiber и передать функцию fiber в конструктор суперкласса.
передается в конструктор суперкласса. В следующем примере функция fiber
является функцией-членом:

.. code-block:: d

    class MyFiber : Fiber
    {
        this()
        {
            super(&run);
        }

        void run()
        {
            // ...
        }
    }
    // ...
    auto fiber = new MyFiber();

* Запуск и возобновление работы волокна осуществляется его функцией call():

.. code-block:: d

    fiber.call();

В отличие от потоков, вызывающая сторона приостанавливается на время выполнения fiber.
* Fiber приостанавливает выполнение (уступает исполнение вызывающему) с помощью Fiber.yield():

.. code-block:: d

    void fiberFunction()
    {
        // ...
        Fiber.yield();
        // ...
    }

Выполнение вызывающей стороны возобновляется, когда fiber завершается.
* Состояние выполнения fiber определяется его свойством .state:

.. code-block:: d

    if (fiber.state == Fiber.State.TERM)
    {
        // ...
    }

Fiber.State - это перечисление со следующими значениями:
* HOLD: Fiber находится в состоянии паузы, что означает, что его можно запустить или возобновить.
* EXEC: fiber в настоящее время выполняется.
* TERM: fiber завершил свою работу. Его необходимо сбросить (функция reset()), прежде чем можно будет использовать
снова.

Fibers в реализациях диапазонов
--------------------------------

Почти каждый диапазон должен хранить некоторую информацию, чтобы помнить о своем состоянии
итерации. Это необходимо для того, чтобы знать, что делать, когда его popFront() будет вызван в следующий раз.
в следующий раз. Большинство примеров диапазонов, которые мы рассматривали в разделе :ref:`Диапазоны<глава_81_start>` и
последующих главах, хранят некое состояние для выполнения своих задач.
Например, FibonacciSeries, который мы определили ранее, хранил две
переменных-членов для вычисления следующего числа в серии:

.. code-block:: d

    struct FibonacciSeries
    {
        int current = 0;
        int next = 1;
        enum empty = false;
        int front() const
        {
            return current;
        }

        void popFront()
        {
            const nextNext = current + next;
            current = next;
            next = nextNext;
        }
    }

Хотя сохранение состояния итерации является тривиальной задачей для некоторых диапазонов, таких как
FibonacciSeries, это на удивление сложнее для некоторых других, например, рекурсивных структур данных
структур данных, таких как деревья двоичного поиска. Причина удивительного поведения в том, что для таких
структур данных, те же алгоритмы тривиальны при рекурсивной реализации.
Например, следующие рекурсивные реализации insert() и
print() не определяют никаких переменных и не зависят от количества
элементов, содержащихся в дереве. Рекурсивные вызовы выделены. (Обратите внимание, что
insert() является рекурсивным косвенно через insertOrSet()).

.. code-block:: d

    import std.stdio;
    import std.string;
    import std.conv;
    import std.random;
    import std.range;
    import std.algorithm;

    /* Represents the nodes of a binary tree. This type is used in
    * the implementation of struct Tree below and should not be
    * used directly. */
    struct Node
    {
        int element;
        Node* left; // Left sub-tree
        Node* right; // Right sub-tree
        void insert(int element)
        {
            if (element < this.element)
            {
                /* Smaller elements go under the left sub-tree. */
                insertOrSet(left, element);
            }
            else if (element > this.element)
            {
                /* Larger elements go under the right sub-tree. */
                insertOrSet(right, element);
            }
            else
            {
                throw new Exception(format("%s already exists",
                        element));
            }
        }

        void print() const
        {
            /* First print the elements of the left sub-tree */
            if (left)
            {
                left.print();
                write(' ');
            }
            /* Then print this element */
            write(element);
            /* Lastly, print the elements of the right sub-tree */
            if (right)
            {
                write(' ');
                right.print();
            }
        }
    }
    /* Inserts the element to the specified sub-tree, potentially
    * initializing its node. */
    void insertOrSet(ref Node* node, int element)
    {
        if (!node)
        {
            /* This is the first element of this sub-tree. */
            node = new Node(element);
        }
        else
        {
            node.insert(element);
        }
    }
    /* This is the actual Tree representation. It allows an empty
    * tree by means of 'root' being equal to 'null'. */
    struct Tree
    {
        Node* root;
        /* Inserts the element to this tree. */
        void insert(int element)
        {
            insertOrSet(root, element);
        }
        /* Prints the elements in sorted order. */
        void print() const
        {
            if (root)
            {
                Fibers 655 root.print();
            }
        }
    }
    /* Populates the tree with 'n' random numbers picked out of a
    * set of '10 * n' numbers. */
    Tree makeRandomTree(size_t n)
    {
        auto numbers = iota((n * 10).to!int)
            .randomSample(n, Random(unpredictableSeed))
            .array;
        randomShuffle(numbers);
        /* Populate the tree with those numbers. */
        auto tree = Tree();
        numbers.each!(e => tree.insert(e));
        return tree;
    }

    void main()
    {
        auto tree = makeRandomTree(10);
        tree.print();
    }


*Примечание: В приведенной выше программе используются следующие возможности Phobos:*

* std.range.iota лениво генерирует элементы заданного диапазона значений (по умолчанию
  умолчанию первым элементом является значение .init). Например, iota(10) - это диапазон
  элементов int от 0 до 9.
* std.algorithm.each похож на std.algorithm.map. В то время как map()
  генерирует новый результат для каждого элемента, each() генерирует побочные эффекты для
  каждого элемента. Кроме того, map() является ленивым, а each() - нетерпеливым.
* std.random.randomSample выбирает случайную выборку элементов из
  заданного диапазона, не изменяя их порядок.
* std.random.randomShuffle случайным образом перемешивает элементы диапазона.
  Как и большинство контейнеров, это дерево должно предоставлять интерфейс диапазона, чтобы
  чтобы его элементы можно было использовать в существующих алгоритмах работы с диапазонами. Это можно сделать, определив
  определения функции-члена opSlice():

.. code-block:: d

    struct Tree
    {
        // ...
        /* Provides access to the elements of the tree in sorted
         * order. */
        struct InOrderRange
        {
            ...What should the implementation be ? ...}
            InOrderRange opSlice() const
            {
                return InOrderRange(root);
            }
        }
    }

Хотя приведенная выше функция print(), по сути, решает ту же задачу
посещение каждого элемента в отсортированном порядке, реализовать InputRange для дерева не так просто.
Я не буду пытаться реализовать InOrderRange здесь, но я
рекомендую вам реализовать или хотя бы изучить итераторы для деревьев. (Некоторые
реализации требуют, чтобы узлы дерева имели дополнительный Node* для указания на
родителя каждого узла).
Причина, по которой рекурсивные древовидные алгоритмы, такие как print(), являются тривиальными, кроется в
автоматического управления стеком вызовов. Стек вызовов неявно содержит
информацию не только о том, что представляет собой текущий элемент, но и о том, как
как выполнение программы пришло к этому элементу (например, в каких узлах
выполнение шло по левому узлу, а не по правому).
Например, когда рекурсивный вызов left.print() возвращается после печати
элементов левого поддерева, локальное состояние текущего вызова print() уже
подразумевает, что теперь пришло время напечатать символ пробела:


.. code-block:: d

    void print() const
    {
        if (left)
        {
            left.print();
            write(' '); // ← Call stack implies this is next
        }
        // ...
    }

Fibers полезны в подобных случаях, когда использование стека вызовов намного проще, чем
хранение состояния в явном виде.
Хотя преимущества fiber не будут очевидны в такой простой задаче, как
генерации ряда Фибоначчи, для простоты рассмотрим общие операции с fibers
на реализации одного fiber. Реализацию древовидного диапазона мы рассмотрим ниже.

.. code-block:: d

    import core.thread;

    /* This is the fiber function that generates each element and
    * then sets the 'ref' parameter to that element. */
    void fibonacciSeries(ref int current)
    { // (1)
        current = 0; // Note that 'current' is the parameter
        int next = 1;
        while (true)
        {
            Fiber.yield(); // (2)
            /* Next call() will continue from this point */ // (3)
            const nextNext = current + next;
            current = next;
            next = nextNext;
        }
    }

    void main()
    {
        int current; // (1)
        // (4)
        Fiber fiber = new Fiber(() => fibonacciSeries(current));
        foreach (_; 0 .. 10)
        {
            fiber.call(); // (5)
            import std.stdio;

            writef("%s ", current);
        }
    }

#. Приведенная выше функция fiber принимает ссылку на int. Она использует этот параметр, чтобы
чтобы сообщить текущий элемент вызывающему ее пользователю. (Параметр можно
квалифицировать как out вместо ref).
#. Когда текущий элемент готов к использованию, волокно приостанавливает свою работу, вызывая
Fiber.yield().
#. При последующем вызове() функция возобновляется сразу после последнего вызова Fiber.yield().
Fiber.yield(). (Первый вызов() запускает функцию).

#. Поскольку волоконные функции не принимают параметров, fibonacciSeries() не может быть использована непосредственно в качестве функции волокна. Вместо этого в качестве адаптера используется делегат без параметров (:ref:`Указатели на функции, делегаты и лямбда-выражения<глава_72_start>`) используется в качестве адаптера, который передается в конструктор Fiber.
#. Вызывающая функция запускает и возобновляет работу волокна с помощью своей функции-члена call().

В результате main() получает элементы серии через current и
печатает их:

.. code-block:: console

    0 1 1 2 3 5 8 13 21 3

**std.concurrency.Generator для представления fiber в виде диапазонов**

Хотя нам удалось сгенерировать ряд Фибоначчи с помощью fiber, эта
эта реализация имеет следующие недостатки:
* Приведенное выше решение не предоставляет интерфейс диапазона, что делает его несовместимым
с существующими алгоритмами определения диапазонов.
* Представление элементов путем изменения параметра ref менее желательно
по сравнению с дизайном, в котором элементы копируются в контекст вызывающей стороны.
* Конструирование и использование fiber явно через его функции-члены
раскрывает детали реализации более низкого уровня по сравнению с альтернативными конструкциями.
Класс std.concurrency.Generator решает все эти проблемы. Обратите внимание, как
fibonacciSeries() ниже написана как простая функция. Единственное отличие заключается в том.
что вместо возврата одного элемента с помощью return, она может сделать доступными несколько
элементов с помощью yield() (в данном примере бесконечное количество элементов).
Также обратите внимание, что на этот раз используется функция std.concurrency.yield, а не
Fiber.yield, которую мы использовали выше.

.. code-block:: d

    import core.thread;

    /* This is the fiber function that generates each element and
    * then sets the 'ref' parameter to that element. */
    void fibonacciSeries(ref int current)
    { // (1)
        current = 0; // Note that 'current' is the parameter
        int next = 1;
        while (true)
        {
            Fiber.yield(); // (2)
            /* Next call() will continue from this point */ // (3)
            const nextNext = current + next;
            current = next;
            next = nextNext;
        }
    }

    void main()
    {
        int current; // (1)
        // (4)
        Fiber fiber = new Fiber(() => fibonacciSeries(current));
        foreach (_; 0 .. 10)
        {
            fiber.call(); // (5)
            import std.stdio;

            writef("%s ", current);
        }
    }

В результате элементы, создаваемые функцией fibers, используются
удобно использовать в качестве InputRange:

.. code-block:: console

    0 1 1 2 3 5 8 13 21 34

Используя Generator, мы можем легко представить элементы дерева в виде InputRange
также. Далее, когда дерево имеет интерфейс InputRange, член print()
больше не будет нужен, поэтому он удаляется. Особенно обратите внимание на то, как
byNode() реализована как адаптер над рекурсивной функцией nextNode():

.. code-block:: d

    import std.concurrency;

    alias FiberRange = std.concurrency.Generator;
    struct Node
    {
        // ...
        /* Note: print() member function is removed because it is
    * not needed anymore. */
        auto opSlice() const
        {
            return byNode(&this);
        }
    }
    /* This is the fiber function that yields the next tree node
    * in sorted order. */
    void nextNode(const(Node)* node)
    {
        if (!node)
        {
            /* No element at or under this node */
            return;
        }
        nextNode(node.left); // First, elements on the left
        yield(node); // Then, this element
        nextNode(node.right); // Finally, elements on the right
    }
    /* Returns an InputRange to the nodes of the tree. */
    auto byNode(const(Node)* node)
    {
        return new FiberRange!(const(Node)*)(
            () => nextNode(node));
    }
    // ...
    struct Tree
    {
        // ...
        /* Note: print() member function is removed because it is
    * not needed anymore. */
        auto opSlice() const
        {
            /* A translation from the nodes to the elements. */
            return byNode(this).map!(n => n.element);
        }
    }
    /* Returns an InputRange to the nodes of the tree. The
    * returned range is empty if the tree has no elements (i.e.
    * if 'root' is 'null'). */
    auto byNode(const(Tree) tree)
    {
        if (tree.root)
        {
            return byNode(tree.root);
        }
        else
        {
            alias RangeType = typeof(return);
            return new RangeType(() {}); // ← Empty range
        }
    }

Теперь объекты деревьев можно разделять на части с помощью [], а результат использовать в качестве
InputRange:

.. code-block:: d

    writefln("%(%s %)", tree[]);

Fibers в асинхронном вводе и выводе
------------------------------------

Стек вызовов волокна может упростить задачи асинхронного ввода и вывода.
также.
В качестве примера представим систему, в которой пользователи регистрируются на сервисе путем
подключаясь к серверу и указывая свое имя, электронную почту и возраст, в таком порядке.
Это похоже на процесс регистрации пользователя на веб-сайте. Чтобы сделать пример
вместо реализации реального веб-сервиса, давайте смоделируем взаимодействие пользователей
взаимодействие пользователей с помощью данных, введенных в командной строке. Используем следующий
простой протокол регистрации, в котором входные данные выделены:
∙hi: Пользователь подключается, и генерируется идентификатор потока
∙id data: Пользователь потока, соответствующего id, вводит следующие ожидаемые
данные. Например, если ожидаемые данные для потока 42 - это имя, то команда
для Алисы будет 42 Алиса.
∙bye: Выход из программы
Например, ниже может быть представлено взаимодействие Алисы и Боба, где
входы в программу моделирования выделены. Каждый пользователь подключается и затем
указывает имя, электронную почту и возраст:

.. code-block:: console

    > hi ← Alice connects
    Flow 0 started.
    > 0 Alice
    > 0 alice@example.com
    > 0 20
    Flow 0 has completed. ← Alice finishes
    Added user 'Alice'.
    > hi ← Bob connects
    Flow 1 started.
    > 1 Bob
    > 1 bob@example.com
    > 1 30
    Flow 1 has completed. ← Bob finishes
    Added user 'Bob'.
    > bye
    Goodbye.
    Users:
    User("Alice", "alice@example.com", 20)
    User("Bob", "bob@example.com", 30

Эта программа может быть спроектирована так, чтобы ожидать команду hi в цикле, а затем вызывать
функцию для получения входных данных от подключенного пользователя:

.. code-block:: d

    if (input == "hi")
    {
        signOnNewUser(); // ← WARNING: Blocking design
    }

Если программа не поддерживает многозадачность, такой дизайн
будет считаться блокирующим, то есть все остальные пользователи будут заблокированы до тех пор, пока
пока текущий пользователь не завершит вход в систему. Это повлияет на
на отзывчивость даже малоиспользуемых сервисов, если пользователям потребуется несколько минут для
предоставления данных.
Существует несколько вариантов, которые делают эту службу неблокируемой, то есть
несколько пользователей могут регистрироваться одновременно:
* Явное ведение задач: Главный поток может породить один поток для каждого пользователя
и передавать входные данные этому потоку с помощью сообщений. Хотя
этот метод работает, он может потребовать синхронизации потоков и может быть
быть медленнее, чем fiber. (Причины такого потенциального снижения производительности
будут объяснены в разделе о кооперативной многозадачности ниже.)
* Поддержание состояния: Программа может принимать более одного входа в систему и
запоминать состояние каждого входа в явном виде. Например, если Алиса ввела
только свое имя, ее состояние должно указывать на то, что следующими входными данными
будет информация о ее электронной почте.
В качестве альтернативы конструкция на основе fibers может использовать одно волокно на поток входа. Это
позволит реализовать поток линейно, в точном соответствии с протоколом
в точном соответствии с протоколом: сначала имя, затем электронная почта и, наконец, возраст. Например, run()
ниже не нужно делать ничего особенного, чтобы запомнить состояние потока регистрации
потока. Когда его вызывают в следующий раз, он продолжает работу сразу после последнего вызова
Fiber.yield(), который его приостановил. Следующая строка, которая будет выполнена, подразумевается
стек вызовов.
В отличие от предыдущих примеров, в следующей программе используется подкласс Fiber:


.. code-block:: d

    import std.stdio;
    import std.string;
    import std.format;
    import std.exception;
    import std.conv;
    import std.array;
    import core.thread;

    struct User
    {
        string name;
        string email;
        uint age;
    }
    /* This Fiber subclass represents the sign-on flow of a
    * user. */
    class SignOnFlow : Fiber
    {
        /* The data read most recently for this flow. */
        string inputData_;
        /* The information to construct a User object. */
        string name;
        string email;
        uint age;
        this()
        {
            /* Set our 'run' member function as the starting point
    * of the fiber. */
            super(&run);
        }

        void run()
        {
            /* First input is name. */
            name = inputData_;
            Fiber.yield();
            /* Second input is email. */
            email = inputData_;
            Fiber.yield();
            /* Last input is age. */
            age = inputData_.to!uint;
            /* At this point we have collected all information to
    * construct the user. We now "return" instead of
    * 'Fiber.yield()'. As a result, the state of this
    * fiber becomes Fiber.State.TERM. */
        }
        /* This property function is to receive data from the
    * caller. */
        void inputData(string data)
        {
            inputData_ = data;
        }
        /* This property function is to construct a user and
    * return it to the caller. */
        User user() const
        {
            return User(name, email, age);
        }
    }
    /* Represents data read from the input for a specific flow. */
    struct FlowData
    {
        size_t id;
        string data;
    }
    /* Parses data related to a flow. */
    FlowData parseFlowData(string line)
    {
        size_t id;
        string data;
        const items = line.formattedRead!" %s %s"(id, data);
        enforce(items == 2, format("Bad input '%s'.", line));
        return FlowData(id, data);
    }

    void main()
    {
        User[] users;
        SignOnFlow[] flows;
        bool done = false;
        while (!done)
        {
            write("> ");
            string line = readln.strip;
            switch (line)
            {
            case "hi":
                /* Start a flow for the new connection. */
                flows ~= new SignOnFlow();
                writefln("Flow %s started.", flows.length - 1);
                break;
            case "bye":
                /* Exit the program. */
                done = true;
                break;
            default:
                /* Try to use the input as flow data. */
                try
                {
                    auto user = handleFlowData(line, flows);
                    if (!user.name.empty)
                    {
                        users ~= user;
                        writefln("Added user '%s'.", user.name);
                    }
                }
                catch (Exception exc)
                {
                    writefln("Error: %s", exc.msg);
                }
                break;
            }
        }
        writeln("Goodbye.");
        writefln("Users:\n%(  %s\n%)", users);
    }
    /* Identifies the owner fiber for the input, sets its input
    * data, and resumes that fiber. Returns a user with valid
    * fields if the flow has been completed. */
    User handleFlowData(string line, SignOnFlow[] flows)
    {
        const input = parseFlowData(line);
        const id = input.id;
        enforce(id < flows.length, format("Invalid id: %s.", id));
        auto flow = flows[id];
        enforce(flow.state == Fiber.State.HOLD,
            format("Flow %s is not runnable.", id));
        /* Set flow data. */
        flow.inputData = input.data;
        /* Resume the flow. */
        flow.call();
        User user;
        if (flow.state == Fiber.State.TERM)
        {
            writefln("Flow %s has completed.", id);
            /* Set the return value to the newly created user. */
            user = flow.user;
            /* TODO: This fiber's entry in the 'flows' array can
    * be reused for a new flow in the future. However, it
    * must first be reset by 'flow.reset()'. */
        }
        return user;
    }

main() считывает строки из входных данных, разбирает их и отправляет данные потока в
соответствующему потоку для обработки. Стек вызовов каждого потока поддерживает состояние потока
состояние автоматически. Новые пользователи добавляются в систему, когда становится доступна полная
информация о пользователях становится доступной.
Запустив приведенную выше программу, вы увидите, что независимо от того, сколько времени требуется пользователю
чтобы завершить свой индивидуальный поток регистрации, система всегда принимает новые пользовательские
соединения. В качестве примера выделено взаимодействие Алисы:

.. code-block:: console

    > hi ← Alice connects
    Flow 0 started.
    > 0 Alice
    > hi ← Bob connects
    Flow 1 started.
    > hi ← Cindy connects
    Flow 2 started.
    > 0 alice@example.com
    > 1 Bob
    > 2 Cindy
    > 2 cindy@example.com
    > 2 40 ← Cindy finishes
    Flow 2 has completed.
    Added user 'Cindy'.
    > 1 bob@example.com
    > 1 30 ← Bob finishes
    Flow 1 has completed.
    Added user 'Bob'.
    > 0 20 ← Alice finishes
    Flow 0 has completed.
    Added user 'Alice'.
    > bye
    Goodbye.
    Users:
    User("Cindy", "cindy@example.com", 40)
    User("Bob", "bob@example.com", 30)
    User("Alice", "alice@example.com", 20

Хотя Алиса, Боб и Синди подключаются в таком порядке, они завершают процесс регистрации
в разном темпе. В результате массив пользователей заполняется в том порядке,  в каком заершаются потоки.
Одно из преимуществ использования fibers в этой программе заключается в том, что SignOnFlow.run()
написан тривиально, без учета того, насколько быстро или медленно пользователь вводил данные.
Кроме того, ни один пользователь не будет заблокирован, когда другие потоки регистрации находятся в процессе.
Многие фреймворки асинхронного ввода/вывода, такие как vibe.d1, используют аналогичные конструкции
основанные на fibers

Исключения и fibers
-------------------

В главе :ref:`Исключения<глава_39_start>`) мы видели, как "объект исключения, который
выброшенный из функции нижнего уровня, передается в функции верхнего уровня
по одному уровню за раз". Мы также видели, что не пойманное исключение "заставляет программу
окончательный выход из функции main()". Хотя в этой главе не упоминался
стек вызовов, описанное поведение механизма исключений достигается с помощью
стека вызовов.
Продолжая первый пример в этой главе, если исключение будет выброшено
внутри bar(), то сначала из стека вызовов будет удален фрейм bar(), затем
foo(), и, наконец, main(). По мере выхода из функций и удаления их фреймов
удаляются из стека вызовов, деструкторы локальных переменных выполняются для
их финальные операции. Процесс выхода из функций и выполнения деструкторов
локальных переменных из-за выброшенного исключения называется разворачиванием стека.
Поскольку fibers имеют свой собственный стек, исключение, брошенное во время
во время выполнения fiber, разворачивает стек вызовов волокна, а не его вызывающего пользователя. Если
исключение не поймано, функция волокна завершается, и состояние волокна
становится Fiber.State.TERM.
Хотя в некоторых случаях такое поведение может быть желательным, иногда fiber
может потребоваться сообщить о состоянии ошибки вызывающей стороне, не теряя при этом своего
состояние выполнения. Fiber.yieldAndThrow позволяет волокну выйти и немедленно
выбросить исключение в контекст вызывающей стороны.
Чтобы увидеть, как это можно использовать, давайте введем в программу регистрации недействительные данные о возрасте:

.. code-block:: console

    > hi
    Flow 0 started.
    > 0 Alice
    > 0 alice@example.com
    > 0 hello ← the user enters invalid age
    Error: Unexpected 'h' when converting from type string to type uint
    > 0 20 ← attempts to correct the error
    Error: Flow 0 is not runnable. ← but the flow is terminated

Вместо того чтобы прерывать fiber и терять весь поток регистрации, fibers может
перехватить ошибку преобразования и сообщить о ней вызывающему модулю с помощью
yieldAndThrow(). Это можно сделать, заменив следующую строку программы
программы, в которой волокно преобразует возрастные данные:

.. code-block:: d

    age = inputData_.to!uint;

Оберните эту строку оператором try-catch внутри безусловного цикла
будет достаточно для поддержания жизни fiber до тех пор, пока не появятся данные, которые можно преобразовать
в uint:

.. code-block:: d

    while (true)
    {
        try
        {
            age = inputData_.to!uint;
            break; // ← Conversion worked; leave the loop
        }
        catch (ConvException exc)
        {
            Fiber.yieldAndThrow(exc);
        }
    }

В этот раз fiber остается в безусловном цикле до тех пор, пока данные не станут достоверными:

.. code-block:: console

    > hi
    Flow 0 started.
    > 0 Alice
    > 0 alice@example.com
    > 0 hello ← the user enters invalid age
    Error: Unexpected 'h' when converting from type string to type uint
    > 0 world ← enters invalid age again
    Error: Unexpected 'w' when converting from type string to type uint
    > 0 20 ← finally, enters valid data
    Flow 0 has completed.
    Added user 'Alice'.
    > bye
    Goodbye.
    Users:
    User("Alice", "alice@example.com", 20)

Как видно из вывода, на этот раз поток регистрации не был потерян, и пользователь
добавлен в систему.

Кооперативная многозадачность
-----------------------------

В отличие от потоков операционной системы, которые приостанавливаются и возобновляются операционной системой в неизвестные моменты времени.
операционной системой в неизвестные моменты времени, fiber приостанавливает себя явным образом
и возобновляется вызывающей стороной явно. Согласно этому различию, вид
многозадачность, которую обеспечивает операционная система, называется вытесняющей многозадачностью
а тот тип, который обеспечивают fibers, называется кооперативной многозадачностью.
При вытесняющей многозадачности операционная система выделяет определенное количество
времени для потока, когда он начинает или возобновляет выполнение. Когда время истекает, этот поток
поток приостанавливается, а вместо него возобновляется другой. Переход от одного потока
к другому называется переключением контекста. Переключение контекста занимает относительно много
времени, которое можно было бы потратить на выполнение реальной работы
потоками.
Учитывая, что система обычно занята большим количеством потоков, переключение контекста
переключение контекста неизбежно и даже желательно. Однако иногда потокам
должны добровольно приостановиться, пока не израсходовали все отведенное им время.
выделенное им. Это может произойти, когда потоку нужна информация от другого
или от устройства. Когда поток приостанавливает свою работу, операционная система должна
снова тратить время на переключение на другой поток. В результате время, которое могло бы
было использовано для выполнения реальной работы, в итоге уходит на переключение контекста.
При использовании fibers вызывающий и вызываемый потоки выполняются как части одного и того же потока. (Это
причина, по которой вызывающая сторона и волокно не могут выполняться одновременно.) В качестве
Преимуществом является отсутствие накладных расходов на переключение контекста между вызывающей стороной и волокном.
(Тем не менее, все еще остаются небольшие накладные расходы, сравнимые с накладными расходами
обычного вызова функции).
Еще одно преимущество кооперативной многозадачности заключается в том, что данные, которыми обмениваются вызывающая и
fibers, с большей вероятностью окажутся в кэше данных процессора. Поскольку данные, которые
находящиеся в кэше процессора, могут быть доступны в сотни раз быстрее, чем данные, которые нужно
считывать из системной памяти, это еще больше повышает производительность
fibers.
Кроме того, поскольку вызывающая программа и fibers никогда не выполняются в одно и то же
одновременно, исключается возможность возникновения условий гонки, что устраняет необходимость в синхронизации данных
синхронизации данных. Однако программист все равно должен убедиться, что волокно выходит в
запланированное время (например, когда данные действительно готовы). Например, вызов func()
ниже не должен выполнять вызов Fiber.yield(), даже косвенно, так как это было бы
преждевременно, до того как значение sharedData будет удвоено:

.. code-block:: d

    void fiberFunction()
    {
        // ...
        func(); // ← must not yield prematurely
        sharedData *= 2;
        Fiber.yield(); // ← intended point to yield
        // ...
    }

Один из очевидных недостатков fibers заключается в том, что только одно ядро процессора используется для
вызывающего устройства и его волокон. Остальные ядра процессора могут простаивать, эффективно
тратя ресурсы впустую. Можно использовать различные конструкции, например модель потоков M:N.
(гибридная многопоточность), которые задействуют и другие ядра. Я рекомендую вам
изучить и сравнить различные модели потоков.

Резюме
------
* Стек вызовов позволяет эффективно распределять локальное состояние и упрощает некоторые
  алгоритмы, особенно рекурсивные.
* fibers позволяют использовать несколько стеков вызовов для каждого потока вместо одного стека вызовов по умолчанию
  вместо одного стека вызовов по умолчанию.
* Fiber и его вызывающая программа выполняются в одном потоке (т. е. не в одно и то же
  время).
* Fiber приостанавливает свою работу, уступая место вызывающему, а вызывающий возобновляет работу волокна, вызывая его снова.
  вызывая его снова.
* Генератор представляет волокно в виде InputRange.
* Fibers упрощают алгоритмы, которые в значительной степени зависят от стека вызовов.
* Fibers упрощают асинхронные операции ввода/вывода.
* Fibers обеспечивают кооперативную многозадачность, которая отличается от
  вытесняющей многозадачности.
